/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/

#include "highlightdefinitionhandler.h"
#include "highlightdefinition.h"
#include "highlighterexception.h"
#include "specificrules.h"
#include "itemdata.h"
#include "keywordlist.h"
#include "context.h"
#include "manager.h"

#include <coreplugin/messagemanager.h>

#include <QCoreApplication>
#include <QLatin1String>

using namespace TextEditor;
using namespace Internal;

namespace {
    static const QLatin1String kName("name");
    static const QLatin1String kList("list");
    static const QLatin1String kItem("item");
    static const QLatin1String kContext("context");
    static const QLatin1String kAttribute("attribute");
    static const QLatin1String kDynamic("dynamic");
    static const QLatin1String kFallthrough("fallthrough");
    static const QLatin1String kLineEndContext("lineEndContext");
    static const QLatin1String kLineEmptyContext("lineEmptyContext");
    static const QLatin1String kLineBeginContext("lineBeginContext");
    static const QLatin1String kFallthroughContext("fallthroughContext");
    static const QLatin1String kBeginRegion("beginRegion");
    static const QLatin1String kEndRegion("endRegion");
    static const QLatin1String kLookAhead("lookAhead");
    static const QLatin1String kFirstNonSpace("firstNonSpace");
    static const QLatin1String kColumn("column");
    static const QLatin1String kItemData("itemData");
    static const QLatin1String kDefStyleNum("defStyleNum");
    static const QLatin1String kColor("color");
    static const QLatin1String kSelColor("selColor");
    static const QLatin1String kItalic("italic");
    static const QLatin1String kBold("bold");
    static const QLatin1String kUnderline("underline");
    static const QLatin1String kStrikeout("strikeout");
    static const QLatin1String kChar("char");
    static const QLatin1String kChar1("char1");
    static const QLatin1String kString("String");
    static const QLatin1String kInsensitive("insensitive");
    static const QLatin1String kMinimal("minimal");
    static const QLatin1String kKeywords("keywords");
    static const QLatin1String kCaseSensitive("casesensitive");
    static const QLatin1String kWeakDeliminator("weakDeliminator");
    static const QLatin1String kAdditionalDeliminator("additionalDeliminator");
    static const QLatin1String kComment("comment");
    static const QLatin1String kPosition("position");
    static const QLatin1String kSingleLine("singleline");
    static const QLatin1String kMultiLine("multiline");
    static const QLatin1String kStart("start");
    static const QLatin1String kEnd("end");
    static const QLatin1String kRegion("region");
    static const QLatin1String kDetectChar("DetectChar");
    static const QLatin1String kDetect2Chars("Detect2Chars");
    static const QLatin1String kAnyChar("AnyChar");
    static const QLatin1String kStringDetect("StringDetect");
    static const QLatin1String kRegExpr("RegExpr");
    static const QLatin1String kKeyword("keyword");
    static const QLatin1String kInt("Int");
    static const QLatin1String kFloat("Float");
    static const QLatin1String kHlCOct("HlCOct");
    static const QLatin1String kHlCHex("HlCHex");
    static const QLatin1String kHlCStringChar("HlCStringChar");
    static const QLatin1String kHlCChar("HlCChar");
    static const QLatin1String kRangeDetect("RangeDetect");
    static const QLatin1String kLineContinue("LineContinue");
    static const QLatin1String kIncludeRules("IncludeRules");
    static const QLatin1String kDetectSpaces("DetectSpaces");
    static const QLatin1String kDetectIdentifier("DetectIdentifier");
    static const QLatin1String kIncludeAttrib("includeAttrib");
    static const QLatin1String kFolding("folding");
    static const QLatin1String kIndentationSensitive("indentationsensitive");
    static const QLatin1String kHash("#");
    static const QLatin1String kDoubleHash("##");
}

HighlightDefinitionHandler::
HighlightDefinitionHandler(const QSharedPointer<HighlightDefinition> &definition) :
    m_definition(definition),
    m_processingKeyword(false),
    m_initialContext(true)
{}

HighlightDefinitionHandler::~HighlightDefinitionHandler()
{}

bool HighlightDefinitionHandler::startDocument()
{
    return true;
}

bool HighlightDefinitionHandler::endDocument()
{
    processIncludeRules();
    return true;
}

bool HighlightDefinitionHandler::startElement(const QString &,
                                              const QString &,
                                              const QString &qName,
                                              const QXmlAttributes &atts)
{
    if (qName == kList)
        listElementStarted(atts);
    else if (qName == kItem)
        itemElementStarted();
    else if (qName == kContext)
        contextElementStarted(atts);
    else if (qName == kItemData)
        itemDataElementStarted(atts);
    else if (qName == kComment)
        commentElementStarted(atts);
    else if (qName == kKeywords)
        keywordsElementStarted(atts);
    else if (qName == kFolding)
        foldingElementStarted(atts);
    else if (qName == kDetectChar)
        detectCharStarted(atts);
    else if (qName == kDetect2Chars)
        detect2CharsStarted(atts);
    else if (qName == kAnyChar)
        anyCharStarted(atts);
    else if (qName == kStringDetect)
        stringDetectedStarted(atts);
    else if (qName == kRegExpr)
        regExprStarted(atts);
    else if (qName == kKeyword)
        keywordStarted(atts);
    else if (qName == kInt)
        intStarted(atts);
    else if (qName == kFloat)
        floatStarted(atts);
    else if (qName == kHlCOct)
        hlCOctStarted(atts);
    else if (qName == kHlCHex)
        hlCHexStarted(atts);
    else if (qName == kHlCStringChar)
        hlCStringCharStarted(atts);
    else if (qName == kHlCChar)
        hlCCharStarted(atts);
    else if (qName == kRangeDetect)
        rangeDetectStarted(atts);
    else if (qName == kLineContinue)
        lineContinue(atts);
    else if (qName == kIncludeRules)
        includeRulesStarted(atts);
    else if (qName == kDetectSpaces)
        detectSpacesStarted(atts);
    else if (qName == kDetectIdentifier)
        detectIdentifier(atts);

    return true;
}

bool HighlightDefinitionHandler::endElement(const QString &, const QString &, const QString &qName)
{
    if (qName == kItem) {
        m_currentList->addKeyword(m_currentKeyword.trimmed());
        m_processingKeyword = false;
    } else if (qName == kDetectChar || qName == kDetect2Chars || qName == kAnyChar ||
               qName == kStringDetect || qName == kRegExpr || qName == kKeyword || qName == kInt ||
               qName == kFloat || qName == kHlCOct || qName == kHlCHex || qName == kHlCStringChar ||
               qName == kHlCChar || qName == kRangeDetect || qName == kLineContinue ||
               qName == kDetectSpaces || qName == kDetectIdentifier) {
        m_currentRule.pop();
    }

    return true;
}

bool HighlightDefinitionHandler::characters(const QString& ch)
{
    // Character data of an element may be reported in more than one chunk.
    if (m_processingKeyword)
        m_currentKeyword.append(ch);

    return true;
}

void HighlightDefinitionHandler::listElementStarted(const QXmlAttributes &atts)
{
    m_currentList = m_definition->createKeywordList(atts.value(kName));
}

void HighlightDefinitionHandler::itemElementStarted()
{
    m_currentKeyword.clear();
    m_processingKeyword = true;
}

void HighlightDefinitionHandler::contextElementStarted(const QXmlAttributes &atts)
{
    m_currentContext = m_definition->createContext(atts.value(kName), m_initialContext);
    m_currentContext->setDefinition(m_definition);
    m_currentContext->setItemData(atts.value(kAttribute));
    m_currentContext->setDynamic(atts.value(kDynamic));
    m_currentContext->setFallthrough(atts.value(kFallthrough));
    m_currentContext->setFallthroughContext(atts.value(kFallthroughContext));
    m_currentContext->setLineBeginContext(atts.value(kLineBeginContext));
    m_currentContext->setLineEndContext(atts.value(kLineEndContext));
    m_currentContext->setLineEmptyContext(atts.value(kLineEmptyContext));

    m_initialContext = false;
}

void HighlightDefinitionHandler::ruleElementStarted(const QXmlAttributes &atts,
                                                    const QSharedPointer<Rule> &rule)
{
    const QString context = atts.value(kContext);
    // The definition of a rule is not necessarily the same of its enclosing context because of
    // externally included rules.
    rule->setDefinition(m_definition);
    rule->setItemData(atts.value(kAttribute));
    rule->setContext(context);
    rule->setBeginRegion(atts.value(kBeginRegion));
    rule->setEndRegion(atts.value(kEndRegion));
    rule->setLookAhead(atts.value(kLookAhead));
    rule->setFirstNonSpace(atts.value(kFirstNonSpace));
    rule->setColumn(atts.value(kColumn));

    if (context.contains(kDoubleHash)) {
        IncludeRulesInstruction includeInstruction(context, m_currentContext->rules().size(), QString());
        m_currentContext->addIncludeRulesInstruction(includeInstruction);
    }
    if (m_currentRule.isEmpty())
        m_currentContext->addRule(rule);
    else
        m_currentRule.top()->addChild(rule);

    m_currentRule.push(rule);
}

void HighlightDefinitionHandler::itemDataElementStarted(const QXmlAttributes &atts) const
{
    QSharedPointer<ItemData> itemData = m_definition->createItemData(atts.value(kName));
    itemData->setStyle(atts.value(kDefStyleNum));
    itemData->setColor(atts.value(kColor));
    itemData->setSelectionColor(atts.value(kSelColor));
    itemData->setItalic(atts.value(kItalic));
    itemData->setBold(atts.value(kBold));
    itemData->setUnderlined(atts.value(kUnderline));
    itemData->setStrikeOut(atts.value(kStrikeout));
}

void HighlightDefinitionHandler::commentElementStarted(const QXmlAttributes &atts) const
{
    const QString &commentType = atts.value(kName);
    if (commentType.compare(kSingleLine, Qt::CaseInsensitive) == 0) {
        m_definition->setSingleLineComment(atts.value(kStart));
        m_definition->setCommentAfterWhitespaces(atts.value(kPosition));
    } else if (commentType.compare(kMultiLine, Qt::CaseInsensitive) == 0) {
        m_definition->setMultiLineCommentStart(atts.value(kStart));
        m_definition->setMultiLineCommentEnd(atts.value(kEnd));
        m_definition->setMultiLineCommentRegion(atts.value(kRegion));
    }
}

void HighlightDefinitionHandler::keywordsElementStarted(const QXmlAttributes &atts) const
{
    // Global case sensitivity appears last in the document (required by dtd) and is set here.
    m_definition->setKeywordsSensitive(atts.value(kCaseSensitive));
    m_definition->removeDelimiters(atts.value(kWeakDeliminator));
    m_definition->addDelimiters(atts.value(kAdditionalDeliminator));
    //@todo: wordWrapDelimiters?
}

void HighlightDefinitionHandler::foldingElementStarted(const QXmlAttributes &atts) const
{
    m_definition->setIndentationBasedFolding(atts.value(kIndentationSensitive));
}

void HighlightDefinitionHandler::detectCharStarted(const QXmlAttributes &atts)
{
    DetectCharRule *rule = new DetectCharRule;
    rule->setChar(atts.value(kChar));
    rule->setActive(atts.value(kDynamic));
    ruleElementStarted(atts, QSharedPointer<Rule>(rule));
}

void HighlightDefinitionHandler::detect2CharsStarted(const QXmlAttributes &atts)
{
    Detect2CharsRule *rule = new Detect2CharsRule;
    rule->setChar(atts.value(kChar));
    rule->setChar1(atts.value(kChar1));
    rule->setActive(atts.value(kDynamic));
    ruleElementStarted(atts, QSharedPointer<Rule>(rule));
}

void HighlightDefinitionHandler::anyCharStarted(const QXmlAttributes &atts)
{
    AnyCharRule *rule = new AnyCharRule;
    rule->setCharacterSet(atts.value(kString));
    ruleElementStarted(atts, QSharedPointer<Rule>(rule));
}

void HighlightDefinitionHandler::stringDetectedStarted(const QXmlAttributes &atts)
{
    StringDetectRule *rule = new StringDetectRule;
    rule->setString(atts.value(kString));
    rule->setInsensitive(atts.value(kInsensitive));
    rule->setActive(atts.value(kDynamic));
    ruleElementStarted(atts, QSharedPointer<Rule>(rule));
}

void HighlightDefinitionHandler::regExprStarted(const QXmlAttributes &atts)
{
    RegExprRule *rule = new RegExprRule;
    rule->setPattern(atts.value(kString));
    rule->setMinimal(atts.value(kMinimal));
    rule->setInsensitive(atts.value(kInsensitive));
    rule->setActive(atts.value(kDynamic));
    ruleElementStarted(atts, QSharedPointer<Rule>(rule));
}

void HighlightDefinitionHandler::keywordStarted(const QXmlAttributes &atts)
{
    KeywordRule *rule = new KeywordRule(m_definition);
    try {
        rule->setList(atts.value(kString));
    } catch (const HighlighterException &e) {
        // Handle broken files. makefile.xml references an invalid list.
        Core::MessageManager::write(
                    QCoreApplication::translate("GenericHighlighter",
                                                "Generic highlighter warning: ") + e.message());
    }
    rule->setInsensitive(atts.value(kInsensitive));
    ruleElementStarted(atts, QSharedPointer<Rule>(rule));
}

void HighlightDefinitionHandler::intStarted(const QXmlAttributes &atts)
{
    ruleElementStarted(atts, QSharedPointer<Rule>(new IntRule));
}

void HighlightDefinitionHandler::floatStarted(const QXmlAttributes &atts)
{
    ruleElementStarted(atts, QSharedPointer<Rule>(new FloatRule));
}

void HighlightDefinitionHandler::hlCOctStarted(const QXmlAttributes &atts)
{
    ruleElementStarted(atts, QSharedPointer<Rule>(new HlCOctRule));
}

void HighlightDefinitionHandler::hlCHexStarted(const QXmlAttributes &atts)
{
    ruleElementStarted(atts, QSharedPointer<Rule>(new HlCHexRule));
}

void HighlightDefinitionHandler::hlCStringCharStarted(const QXmlAttributes &atts)
{
    ruleElementStarted(atts, QSharedPointer<Rule>(new HlCStringCharRule));
}

void HighlightDefinitionHandler::hlCCharStarted(const QXmlAttributes &atts)
{
    ruleElementStarted(atts, QSharedPointer<Rule>(new HlCCharRule));
}

void HighlightDefinitionHandler::rangeDetectStarted(const QXmlAttributes &atts)
{
    RangeDetectRule *rule = new RangeDetectRule;
    rule->setChar(atts.value(kChar));
    rule->setChar1(atts.value(kChar1));
    ruleElementStarted(atts, QSharedPointer<Rule>(rule));
}

void HighlightDefinitionHandler::lineContinue(const QXmlAttributes &atts)
{
    ruleElementStarted(atts, QSharedPointer<Rule>(new LineContinueRule));
}

void HighlightDefinitionHandler::includeRulesStarted(const QXmlAttributes &atts)
{
    // Include rules are treated as instructions for latter processing.
    IncludeRulesInstruction instruction(atts.value(kContext), m_currentContext->rules().size(),
                                        atts.value(kIncludeAttrib));

    // Include rules (as many others) are not allowed as a child.
    m_currentContext->addIncludeRulesInstruction(instruction);
}

void HighlightDefinitionHandler::detectSpacesStarted(const QXmlAttributes &atts)
{
    ruleElementStarted(atts, QSharedPointer<Rule>(new DetectSpacesRule));
}

void HighlightDefinitionHandler::detectIdentifier(const QXmlAttributes &atts)
{
    ruleElementStarted(atts, QSharedPointer<Rule>(new DetectIdentifierRule));
}

void HighlightDefinitionHandler::processIncludeRules() const
{
    const QHash<QString, QSharedPointer<Context> > &allContexts = m_definition->contexts();
    foreach (const QSharedPointer<Context> &context, allContexts)
        processIncludeRules(context);
}

void HighlightDefinitionHandler::processIncludeRules(const QSharedPointer<Context> &context) const
{
    if (context->includeRulesInstructions().isEmpty())
        return;

    int rulesIncluded = 0;
    const QList<IncludeRulesInstruction> &instructions = context->includeRulesInstructions();
    foreach (const IncludeRulesInstruction &instruction, instructions) {

        QSharedPointer<Context> sourceContext;
        const QString &sourceName = instruction.sourceContext();
        if (sourceName.contains(kDoubleHash)) {
            // This refers to an external definition. Context can be specified before the double
            // hash (e.g. Normal##Javascript). If it isn't, the rules included are the ones from its
            // initial context. Others contexts and rules from the external definition will work
            // transparently to the highlighter. This is because contexts and rules know the
            // definition they are from.
            const QStringList values = sourceName.split(kDoubleHash);
            if (values.count() != 2)
                return;
            const QString externalContext = values.at(0);
            const QString externalName = values.at(1);
            const QString &id = Manager::instance()->definitionIdByName(externalName);

            // If there is an incorrect circular dependency among definitions this is skipped.
            if (Manager::instance()->isBuildingDefinition(id))
                continue;

            const QSharedPointer<HighlightDefinition> &externalDefinition =
                Manager::instance()->definition(id);
            if (externalDefinition.isNull() || !externalDefinition->isValid())
                continue;

            if (externalContext.isEmpty())
                sourceContext = externalDefinition->initialContext();
            else
                sourceContext = externalDefinition->context(externalContext);
        } else if (!sourceName.startsWith(kHash)) {
            sourceContext = m_definition->context(sourceName);

            // Recursion is done only for context direct rules. Child rules are not processed
            // because they cannot be include rules.
            processIncludeRules(sourceContext);
        } else {
            continue;
        }

        if (instruction.replaceItemData()) {
            context->setItemData(sourceContext->itemData());
            context->setDefinition(sourceContext->definition());
        }

        foreach (QSharedPointer<Rule> rule, sourceContext->rules()) {
            context->addRule(rule, instruction.indexHint() + rulesIncluded);
            ++rulesIncluded;
        }
    }
    context->clearIncludeRulesInstructions();
}
